openmd-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. openmd/__init__.py +3 -0
  2. openmd/__main__.py +5 -0
  3. openmd/adapters/__init__.py +1 -0
  4. openmd/adapters/base.py +95 -0
  5. openmd/adapters/bilibili/__init__.py +11 -0
  6. openmd/adapters/bilibili/adapter.py +104 -0
  7. openmd/adapters/bilibili/discovery.py +33 -0
  8. openmd/adapters/douyin/__init__.py +11 -0
  9. openmd/adapters/douyin/adapter.py +117 -0
  10. openmd/adapters/douyin/discovery.py +36 -0
  11. openmd/adapters/douyin/sidecar.py +77 -0
  12. openmd/adapters/generic_media/__init__.py +14 -0
  13. openmd/adapters/high_privacy.py +77 -0
  14. openmd/adapters/local_audio/__init__.py +14 -0
  15. openmd/adapters/local_audio/adapter.py +169 -0
  16. openmd/adapters/local_audio/asr.py +58 -0
  17. openmd/adapters/local_audio/backends/__init__.py +1 -0
  18. openmd/adapters/local_audio/backends/auto.py +177 -0
  19. openmd/adapters/local_audio/backends/faster_whisper.py +285 -0
  20. openmd/adapters/local_audio/backends/funasr_sense_voice.py +14 -0
  21. openmd/adapters/local_audio/backends/qwen3_asr.py +254 -0
  22. openmd/adapters/opencli.py +770 -0
  23. openmd/adapters/pdf/__init__.py +11 -0
  24. openmd/adapters/pdf/adapter.py +153 -0
  25. openmd/adapters/pdf/backends/__init__.py +1 -0
  26. openmd/adapters/pdf/backends/markitdown.py +165 -0
  27. openmd/adapters/pdf/parser.py +56 -0
  28. openmd/adapters/qq_group/__init__.py +11 -0
  29. openmd/adapters/qq_group/adapter.py +289 -0
  30. openmd/adapters/qq_group/parsers/__init__.py +1 -0
  31. openmd/adapters/qq_group/parsers/html.py +60 -0
  32. openmd/adapters/qq_group/parsers/json.py +71 -0
  33. openmd/adapters/registry.py +149 -0
  34. openmd/adapters/screenshot/__init__.py +11 -0
  35. openmd/adapters/screenshot/adapter.py +154 -0
  36. openmd/adapters/screenshot/backends/__init__.py +1 -0
  37. openmd/adapters/screenshot/backends/paddle.py +15 -0
  38. openmd/adapters/screenshot/backends/rapidocr.py +176 -0
  39. openmd/adapters/screenshot/backends/tesseract.py +15 -0
  40. openmd/adapters/screenshot/ocr.py +84 -0
  41. openmd/adapters/wechat_group/__init__.py +11 -0
  42. openmd/adapters/wechat_group/adapter.py +160 -0
  43. openmd/adapters/wechat_group/wx_cli.py +268 -0
  44. openmd/adapters/wechat_moments/__init__.py +11 -0
  45. openmd/adapters/wechat_moments/adapter.py +167 -0
  46. openmd/adapters/wechat_moments/wx_cli.py +265 -0
  47. openmd/adapters/weixin/__init__.py +11 -0
  48. openmd/adapters/weixin/adapter.py +148 -0
  49. openmd/adapters/weixin/discovery.py +510 -0
  50. openmd/adapters/weixin/report.py +278 -0
  51. openmd/adapters/xiaohongshu.py +158 -0
  52. openmd/backends/__init__.py +1 -0
  53. openmd/backends/base.py +204 -0
  54. openmd/backends/bilibili.py +1480 -0
  55. openmd/backends/bilibili_wbi.py +277 -0
  56. openmd/backends/discourse.py +426 -0
  57. openmd/backends/douyin.py +868 -0
  58. openmd/backends/douyin_detail.py +124 -0
  59. openmd/backends/douyin_providers.py +602 -0
  60. openmd/backends/executor.py +546 -0
  61. openmd/backends/feishu.py +861 -0
  62. openmd/backends/generic_media.py +77 -0
  63. openmd/backends/interactive.py +60 -0
  64. openmd/backends/ocr.py +41 -0
  65. openmd/backends/opencli.py +82 -0
  66. openmd/backends/pdf.py +122 -0
  67. openmd/backends/registry.py +234 -0
  68. openmd/backends/screenshot.py +79 -0
  69. openmd/backends/video_metadata.py +167 -0
  70. openmd/backends/web_article.py +363 -0
  71. openmd/backends/wechat_group.py +35 -0
  72. openmd/backends/wechat_moments.py +35 -0
  73. openmd/backends/weixin.py +767 -0
  74. openmd/backends/x.py +455 -0
  75. openmd/backends/xiaoe.py +940 -0
  76. openmd/backends/xiaohongshu.py +911 -0
  77. openmd/backends/xiaohongshu_providers.py +876 -0
  78. openmd/backends/xiaoyuzhou.py +1017 -0
  79. openmd/backends/youtube.py +280 -0
  80. openmd/backends/youtube_discovery.py +1245 -0
  81. openmd/backends/youtube_transcript.py +213 -0
  82. openmd/browser_extension/background.js +231 -0
  83. openmd/browser_extension/icons/icon-128.png +0 -0
  84. openmd/browser_extension/icons/icon-16.png +0 -0
  85. openmd/browser_extension/icons/icon-32.png +0 -0
  86. openmd/browser_extension/icons/icon-48.png +0 -0
  87. openmd/browser_extension/icons/icon-512.png +0 -0
  88. openmd/browser_extension/idle.html +6 -0
  89. openmd/browser_extension/manifest.json +43 -0
  90. openmd/browser_extension/options.html +42 -0
  91. openmd/browser_extension/options.js +117 -0
  92. openmd/browser_extension/popup.css +71 -0
  93. openmd/browser_extension/popup.html +17 -0
  94. openmd/browser_extension/popup.js +59 -0
  95. openmd/browser_extension/vendor/opencli-bridge/LICENSE +190 -0
  96. openmd/browser_extension/vendor/opencli-bridge/VENDOR.md +31 -0
  97. openmd/browser_extension/vendor/opencli-bridge/background.js +2742 -0
  98. openmd/browser_host.py +503 -0
  99. openmd/browser_setup.py +106 -0
  100. openmd/cli.py +2012 -0
  101. openmd/console.py +32 -0
  102. openmd/core/__init__.py +1 -0
  103. openmd/core/capabilities.py +162 -0
  104. openmd/core/content_validation.py +323 -0
  105. openmd/core/coverage.py +163 -0
  106. openmd/core/detector.py +247 -0
  107. openmd/core/doctor.py +104 -0
  108. openmd/core/execution_policy.py +164 -0
  109. openmd/core/filename.py +64 -0
  110. openmd/core/input_validation.py +91 -0
  111. openmd/core/local_config.py +96 -0
  112. openmd/core/manifest.py +156 -0
  113. openmd/core/models.py +168 -0
  114. openmd/core/opencli_output.py +34 -0
  115. openmd/core/operation_contract.py +60 -0
  116. openmd/core/output.py +194 -0
  117. openmd/core/package_contract.py +335 -0
  118. openmd/core/package_validator.py +489 -0
  119. openmd/core/package_writer.py +666 -0
  120. openmd/core/planner.py +345 -0
  121. openmd/core/preferences.py +61 -0
  122. openmd/core/readable.py +129 -0
  123. openmd/core/receipt.py +285 -0
  124. openmd/core/renderer.py +185 -0
  125. openmd/core/runlog.py +208 -0
  126. openmd/core/secrets.py +313 -0
  127. openmd/doctor_view.py +68 -0
  128. openmd/interactive.py +453 -0
  129. openmd/ocr/__init__.py +1 -0
  130. openmd/ocr/breakers.py +84 -0
  131. openmd/ocr/config.py +54 -0
  132. openmd/ocr/documents.py +522 -0
  133. openmd/ocr/endpoint.py +72 -0
  134. openmd/ocr/layout.py +92 -0
  135. openmd/ocr/layout_filter.py +118 -0
  136. openmd/ocr/layout_worker.py +89 -0
  137. openmd/ocr/native_math.py +220 -0
  138. openmd/ocr/office.py +305 -0
  139. openmd/ocr/ollama.py +200 -0
  140. openmd/ocr/otsl.py +51 -0
  141. openmd/ocr/ovis-prompt.txt +1 -0
  142. openmd/ocr/pipeline.py +384 -0
  143. openmd/ocr/settings.json +25 -0
  144. openmd/ocr/tiling.py +631 -0
  145. openmd/post_video.py +154 -0
  146. openmd/probe.py +397 -0
  147. openmd/routing/__init__.py +1 -0
  148. openmd/routing/cards/builtin.json +620 -0
  149. openmd/routing/cards/defuddle.json +70 -0
  150. openmd/routing/cards/x-tweet-fetcher.json +71 -0
  151. openmd/routing/cards/xyz-dl.json +60 -0
  152. openmd/routing/cards/yutto.json +68 -0
  153. openmd/routing/entry_shim.py +71 -0
  154. openmd/routing/lifecycle.py +459 -0
  155. openmd/routing/loader.py +138 -0
  156. openmd/routing/providers.py +158 -0
  157. openmd/routing/readers.py +108 -0
  158. openmd/routing/routes.json +504 -0
  159. openmd/routing/runner.py +192 -0
  160. openmd/routing/view.py +40 -0
  161. openmd/runtime/__init__.py +1 -0
  162. openmd/runtime/_windows_job.py +234 -0
  163. openmd/runtime/asr.py +217 -0
  164. openmd/runtime/asr_worker.py +119 -0
  165. openmd/runtime/gpu_lock.py +81 -0
  166. openmd/runtime/gpu_quarantine.py +85 -0
  167. openmd/runtime/interpreters.py +407 -0
  168. openmd/runtime/managed_tools.py +204 -0
  169. openmd/runtime/models.py +202 -0
  170. openmd/runtime/services.py +175 -0
  171. openmd/runtime/tempdirs.py +227 -0
  172. openmd_cli-0.1.0.dist-info/METADATA +168 -0
  173. openmd_cli-0.1.0.dist-info/RECORD +177 -0
  174. openmd_cli-0.1.0.dist-info/WHEEL +5 -0
  175. openmd_cli-0.1.0.dist-info/entry_points.txt +2 -0
  176. openmd_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  177. openmd_cli-0.1.0.dist-info/top_level.txt +1 -0
openmd/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Openmd package."""
2
+
3
+ __version__ = "0.1.0"
openmd/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Platform adapter interfaces and registry."""
@@ -0,0 +1,95 @@
1
+ """Platform adapter interface and skeleton adapter implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from openmd.core.detector import detect_platform, extract_first_url
8
+ from openmd.core.models import ContentItem, TranscriptResult
9
+
10
+
11
+ class PlatformAdapter(ABC):
12
+ id: str
13
+
14
+ @abstractmethod
15
+ def can_handle(self, input_value: str) -> bool:
16
+ raise NotImplementedError
17
+
18
+ @abstractmethod
19
+ def resolve_items(
20
+ self, input_value: str, *, limit: int | None = None, all: bool = False
21
+ ) -> list[ContentItem]:
22
+ raise NotImplementedError
23
+
24
+ @abstractmethod
25
+ def extract_text(
26
+ self,
27
+ item: ContentItem,
28
+ *,
29
+ backend: str | None = None,
30
+ backend_chain: tuple[str, ...] | None = None,
31
+ allow_fallback: bool = True,
32
+ ) -> TranscriptResult:
33
+ raise NotImplementedError
34
+
35
+
36
+ class StubPlatformAdapter(PlatformAdapter):
37
+ """Read-only adapter used until real platform backends are wired."""
38
+
39
+ def __init__(self, platform_id: str, default_backend: str) -> None:
40
+ self.id = platform_id
41
+ self.default_backend = default_backend
42
+
43
+ def can_handle(self, input_value: str) -> bool:
44
+ return detect_platform(input_value) == self.id
45
+
46
+ def resolve_items(
47
+ self, input_value: str, *, limit: int | None = None, all: bool = False
48
+ ) -> list[ContentItem]:
49
+ input_type = "collection" if limit is not None or all else "single"
50
+ source_type = "video" if self.id in {"bilibili", "youtube", "douyin", "tiktok"} else "article"
51
+ if self.id in {"xiaoyuzhou"}:
52
+ source_type = "audio"
53
+ if self.id in {"feishu"}:
54
+ source_type = "document"
55
+ return [
56
+ ContentItem(
57
+ platform=self.id,
58
+ input_type=input_type,
59
+ url=extract_first_url(input_value),
60
+ source_type=source_type,
61
+ )
62
+ ]
63
+
64
+ def extract_text(
65
+ self,
66
+ item: ContentItem,
67
+ *,
68
+ backend: str | None = None,
69
+ backend_chain: tuple[str, ...] | None = None,
70
+ allow_fallback: bool = True,
71
+ ) -> TranscriptResult:
72
+ chain = backend_chain or (backend or self.default_backend,)
73
+ if not allow_fallback:
74
+ chain = chain[:1]
75
+ selected_backend = chain[0] if chain else self.default_backend
76
+ if selected_backend == "mock":
77
+ return TranscriptResult(
78
+ text="[00:00:00] Mock transcript for Openmd skeleton.\n",
79
+ status="verified",
80
+ source_level="third_party_extractor",
81
+ backend="mock",
82
+ source_notes=["local smoke check only"],
83
+ )
84
+ return TranscriptResult(
85
+ text="",
86
+ status="failed",
87
+ source_level="partial_extraction",
88
+ backend=selected_backend,
89
+ source_notes=[
90
+ "adapter skeleton only; real extraction not implemented",
91
+ f"backend_chain: {' -> '.join(chain)}",
92
+ ],
93
+ error_code="backend_not_implemented",
94
+ error_summary="Openmd V0 skeleton routed the item but did not run a platform extractor.",
95
+ )
@@ -0,0 +1,11 @@
1
+ """Bilibili V1 platform adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from openmd.adapters.base import PlatformAdapter
6
+
7
+ from .adapter import BilibiliAdapter
8
+
9
+
10
+ def create_adapter(platform_id: str, default_backend: str) -> PlatformAdapter:
11
+ return BilibiliAdapter(platform_id, default_backend)
@@ -0,0 +1,104 @@
1
+ """V1 adapter for Bilibili video/audio-like package inputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+
7
+ from openmd.adapters.base import PlatformAdapter
8
+ from openmd.backends.bilibili import extract_bilibili_uid, extract_bvid
9
+ from openmd.core.detector import detect_platform, extract_first_url
10
+ from openmd.core.models import ContentItem, TranscriptResult
11
+
12
+
13
+ class BilibiliAdapter(PlatformAdapter):
14
+ """Resolve Bilibili BV and UP/collection selectors without side effects."""
15
+
16
+ def __init__(self, platform_id: str = "bilibili", default_backend: str = "bilibili-harvest.core") -> None:
17
+ self.id = platform_id
18
+ self.default_backend = default_backend
19
+
20
+ def can_handle(self, input_value: str) -> bool:
21
+ return detect_platform(input_value) == "bilibili"
22
+
23
+ def resolve_items(
24
+ self,
25
+ input_value: str,
26
+ *,
27
+ limit: int | None = None,
28
+ all: bool = False,
29
+ ) -> list[ContentItem]:
30
+ target = extract_first_url(input_value)
31
+ bvid = extract_bvid(target)
32
+ if bvid is not None:
33
+ return [
34
+ ContentItem(
35
+ platform=self.id,
36
+ input_type="single",
37
+ url=_bilibili_video_url(bvid),
38
+ id=bvid,
39
+ source_type="video",
40
+ raw_meta={
41
+ "profile": "media",
42
+ "permission_scope": "user_accessible",
43
+ "content_id": bvid,
44
+ },
45
+ )
46
+ ]
47
+
48
+ uid = extract_bilibili_uid(target)
49
+ return [
50
+ ContentItem(
51
+ platform=self.id,
52
+ input_type="collection",
53
+ url=f"https://space.bilibili.com/{uid}" if uid else target,
54
+ id=f"space-{uid}" if uid else _stable_id(target),
55
+ source_type="video",
56
+ raw_meta={
57
+ "profile": "media",
58
+ "permission_scope": "user_accessible",
59
+ "collection_selector": input_value.strip(),
60
+ "up_uid": uid,
61
+ "discovery_limit": limit,
62
+ "allow_all": all,
63
+ },
64
+ )
65
+ ]
66
+
67
+ def extract_text(
68
+ self,
69
+ item: ContentItem,
70
+ *,
71
+ backend: str | None = None,
72
+ backend_chain: tuple[str, ...] | None = None,
73
+ allow_fallback: bool = True,
74
+ ) -> TranscriptResult:
75
+ selected = (backend_chain or (backend or self.default_backend,))[0]
76
+ if selected in {"mock", "bilibili.mock"}:
77
+ return TranscriptResult(
78
+ text="[00:00:00] Mock Bilibili transcript for Openmd package smoke.\n",
79
+ status="verified",
80
+ source_level="third_party_extractor",
81
+ backend=selected,
82
+ title=item.title or item.id or "Mock Bilibili item",
83
+ source_notes=["local mock adapter smoke only; no network/browser/OpenCLI/media action was attempted"],
84
+ )
85
+ return TranscriptResult(
86
+ text="",
87
+ status="failed",
88
+ source_level="partial_extraction",
89
+ backend=selected,
90
+ source_notes=[
91
+ "bilibili adapter resolved the item only; real extraction is handled by gated backend providers",
92
+ f"allow_fallback={allow_fallback}",
93
+ ],
94
+ error_code="backend_requires_runtime_gate",
95
+ error_summary="Enable the appropriate Openmd backend gate before running real Bilibili extraction.",
96
+ )
97
+
98
+
99
+ def _bilibili_video_url(bvid: str) -> str:
100
+ return f"https://www.bilibili.com/video/{bvid}/"
101
+
102
+
103
+ def _stable_id(value: str) -> str:
104
+ return hashlib.sha256(value.strip().encode("utf-8")).hexdigest()[:12]
@@ -0,0 +1,33 @@
1
+ """Bilibili discovery lane helpers.
2
+
3
+ The real providers live in ``openmd.backends.bilibili`` and are registered only
4
+ behind explicit network/browser gates. This module keeps adapter-lane imports
5
+ stable without duplicating provider logic.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from openmd.backends.bilibili import (
11
+ BILIBILI_NATIVE_DISCOVERY_BACKEND,
12
+ BILIBILI_OPENCLI_DISCOVERY_BACKEND,
13
+ BilibiliNativeUserVideosProvider,
14
+ OpenCliBilibiliUserVideosProvider,
15
+ extract_bilibili_uid,
16
+ )
17
+
18
+
19
+ def build_opencli_bilibili_user_videos_args(selector: str, *, limit: int | None) -> list[str]:
20
+ uid = extract_bilibili_uid(selector) or selector.strip()
21
+ command = ["opencli", "bilibili", "user-videos", uid]
22
+ if limit is not None:
23
+ command.extend(["--limit", str(max(0, limit))])
24
+ return command
25
+
26
+
27
+ __all__ = [
28
+ "BILIBILI_NATIVE_DISCOVERY_BACKEND",
29
+ "BILIBILI_OPENCLI_DISCOVERY_BACKEND",
30
+ "BilibiliNativeUserVideosProvider",
31
+ "OpenCliBilibiliUserVideosProvider",
32
+ "build_opencli_bilibili_user_videos_args",
33
+ ]
@@ -0,0 +1,11 @@
1
+ """Douyin V1 platform adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from openmd.adapters.base import PlatformAdapter
6
+
7
+ from .adapter import DouyinAdapter
8
+
9
+
10
+ def create_adapter(platform_id: str, default_backend: str) -> PlatformAdapter:
11
+ return DouyinAdapter(platform_id, default_backend)
@@ -0,0 +1,117 @@
1
+ """V1 adapter for Douyin author/video package inputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from urllib.parse import parse_qs, urlparse
7
+
8
+ from openmd.adapters.base import PlatformAdapter
9
+ from openmd.backends.douyin import douyin_user_target, normalize_douyin_collection_url
10
+ from openmd.backends.douyin_detail import douyin_video_id
11
+ from openmd.core.detector import detect_platform, extract_first_url
12
+ from openmd.core.models import ContentItem, TranscriptResult
13
+
14
+
15
+ class DouyinAdapter(PlatformAdapter):
16
+ """Resolve Douyin single video and author selectors without side effects."""
17
+
18
+ def __init__(self, platform_id: str = "douyin", default_backend: str = "douyin.transcript") -> None:
19
+ self.id = platform_id
20
+ self.default_backend = default_backend
21
+
22
+ def can_handle(self, input_value: str) -> bool:
23
+ return detect_platform(input_value) == "douyin"
24
+
25
+ def resolve_items(
26
+ self,
27
+ input_value: str,
28
+ *,
29
+ limit: int | None = None,
30
+ all: bool = False,
31
+ ) -> list[ContentItem]:
32
+ target = extract_first_url(input_value)
33
+ aweme_id = douyin_video_id(target)
34
+ if aweme_id is not None:
35
+ return [
36
+ ContentItem(
37
+ platform=self.id,
38
+ input_type="single",
39
+ url=f"https://www.douyin.com/video/{aweme_id}",
40
+ id=aweme_id,
41
+ source_type="video",
42
+ raw_meta={
43
+ "profile": "media",
44
+ "permission_scope": "user_accessible",
45
+ "content_id": aweme_id,
46
+ },
47
+ )
48
+ ]
49
+
50
+ if (urlparse(target).hostname or "").lower() == "v.douyin.com":
51
+ return [ContentItem(platform=self.id, input_type="single", url=target,
52
+ source_type="video", raw_meta={"profile": "media", "identity_unresolved": True})]
53
+
54
+ sec_uid = douyin_user_target(target)
55
+ normalized = normalize_douyin_collection_url(target) or target
56
+ return [
57
+ ContentItem(
58
+ platform=self.id,
59
+ input_type="collection",
60
+ url=normalized,
61
+ id=f"user-{sec_uid}" if sec_uid else _stable_id(target),
62
+ source_type="video",
63
+ raw_meta={
64
+ "profile": "media",
65
+ "permission_scope": "user_accessible",
66
+ "collection_selector": input_value.strip(),
67
+ "sec_uid": sec_uid,
68
+ "discovery_limit": limit,
69
+ "allow_all": all,
70
+ },
71
+ )
72
+ ]
73
+
74
+ def extract_text(
75
+ self,
76
+ item: ContentItem,
77
+ *,
78
+ backend: str | None = None,
79
+ backend_chain: tuple[str, ...] | None = None,
80
+ allow_fallback: bool = True,
81
+ ) -> TranscriptResult:
82
+ selected = (backend_chain or (backend or self.default_backend,))[0]
83
+ if selected in {"mock", "douyin.mock"}:
84
+ return TranscriptResult(
85
+ text="[00:00:00] Mock Douyin transcript for Openmd package smoke.\n",
86
+ status="verified",
87
+ source_level="platform_transcript",
88
+ backend=selected,
89
+ title=item.title or item.id or "Mock Douyin item",
90
+ source_notes=["local mock adapter smoke only; no sidecar/OpenCLI/browser/account/media/ASR action was attempted"],
91
+ )
92
+ return TranscriptResult(
93
+ text="",
94
+ status="failed",
95
+ source_level="partial_extraction",
96
+ backend=selected,
97
+ source_notes=[
98
+ "douyin adapter resolved the item only; real acquisition is handled by gated discovery/backend providers",
99
+ f"allow_fallback={allow_fallback}",
100
+ ],
101
+ error_code="backend_requires_runtime_gate",
102
+ error_summary="Enable the appropriate Openmd backend gate before running real Douyin acquisition.",
103
+ )
104
+
105
+
106
+ def _douyin_aweme_id(value: str) -> str | None:
107
+ parsed = urlparse(value if "://" in value else f"https://{value}")
108
+ parts = [part for part in parsed.path.split("/") if part]
109
+ if parts[:1] == ["video"] and len(parts) >= 2 and parts[1].isdigit():
110
+ return parts[1]
111
+ query = parse_qs(parsed.query)
112
+ modal = query.get("modal_id", [""])[0].strip()
113
+ return modal if modal.isdigit() else None
114
+
115
+
116
+ def _stable_id(value: str) -> str:
117
+ return hashlib.sha256(value.strip().encode("utf-8")).hexdigest()[:12]
@@ -0,0 +1,36 @@
1
+ """Douyin discovery lane helpers.
2
+
3
+ The real bounded OpenCLI discovery provider lives in ``openmd.backends.douyin``
4
+ and is registered only behind the explicit browser backend gate.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from openmd.backends.douyin import (
10
+ DOUYIN_OPENMD_CREATOR_VIDEOS_BACKEND,
11
+ DOUYIN_OPENCLI_USER_VIDEOS_BACKEND,
12
+ OpenCliOpenmdDouyinCreatorVideosProvider,
13
+ OpenCliDouyinUserVideosProvider,
14
+ build_opencli_openmd_douyin_creator_videos_args,
15
+ build_opencli_douyin_user_videos_args,
16
+ classify_douyin_input_failure,
17
+ douyin_user_target,
18
+ normalize_douyin_collection_url,
19
+ parse_opencli_douyin_user_videos,
20
+ parse_opencli_openmd_douyin_creator_videos,
21
+ )
22
+
23
+
24
+ __all__ = [
25
+ "DOUYIN_OPENMD_CREATOR_VIDEOS_BACKEND",
26
+ "DOUYIN_OPENCLI_USER_VIDEOS_BACKEND",
27
+ "OpenCliOpenmdDouyinCreatorVideosProvider",
28
+ "OpenCliDouyinUserVideosProvider",
29
+ "build_opencli_openmd_douyin_creator_videos_args",
30
+ "build_opencli_douyin_user_videos_args",
31
+ "classify_douyin_input_failure",
32
+ "douyin_user_target",
33
+ "normalize_douyin_collection_url",
34
+ "parse_opencli_douyin_user_videos",
35
+ "parse_opencli_openmd_douyin_creator_videos",
36
+ ]
@@ -0,0 +1,77 @@
1
+ """Read-only safety envelope for an optional Douyin local sidecar."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import urljoin
6
+
7
+
8
+ ALLOWED_SIDECAR_ENDPOINTS: frozenset[tuple[str, str]] = frozenset(
9
+ {
10
+ ("GET", "/api/v1/monitors"),
11
+ ("GET", "/api/v1/contents"),
12
+ ("GET", "/api/v1/contents/:id"),
13
+ ("GET", "/api/v1/contents/:id/comments"),
14
+ }
15
+ )
16
+
17
+ FORBIDDEN_SIDECAR_PATH_PREFIXES: tuple[str, ...] = (
18
+ "/auth/",
19
+ "/api/v1/accounts",
20
+ "/api/v1/admin",
21
+ "/api/v1/creator",
22
+ )
23
+
24
+ FORBIDDEN_SIDECAR_PATH_PARTS: tuple[str, ...] = (
25
+ "/upload",
26
+ "/cancel-upload",
27
+ "/retry-download",
28
+ "/publish",
29
+ "/draft",
30
+ "/delete",
31
+ "/reply",
32
+ "/like",
33
+ "/approve",
34
+ )
35
+
36
+
37
+ def is_allowed_sidecar_endpoint(method: str, path: str) -> bool:
38
+ normalized_method = method.upper().strip()
39
+ normalized_path = _normalize_path(path)
40
+ if normalized_method != "GET":
41
+ return False
42
+ if any(normalized_path.startswith(prefix) for prefix in FORBIDDEN_SIDECAR_PATH_PREFIXES):
43
+ return False
44
+ if any(part in normalized_path for part in FORBIDDEN_SIDECAR_PATH_PARTS):
45
+ return False
46
+ if (normalized_method, normalized_path) in ALLOWED_SIDECAR_ENDPOINTS:
47
+ return True
48
+ return _is_content_detail_path(normalized_method, normalized_path) or _is_content_comments_path(
49
+ normalized_method, normalized_path
50
+ )
51
+
52
+
53
+ def build_sidecar_url(base_url: str, path: str) -> str:
54
+ normalized_base = base_url.rstrip("/") + "/"
55
+ normalized_path = _normalize_path(path).lstrip("/")
56
+ return urljoin(normalized_base, normalized_path)
57
+
58
+
59
+ def _is_content_detail_path(method: str, path: str) -> bool:
60
+ if method != "GET":
61
+ return False
62
+ parts = [part for part in path.split("/") if part]
63
+ return len(parts) == 4 and parts[:3] == ["api", "v1", "contents"] and parts[3].isdigit()
64
+
65
+
66
+ def _is_content_comments_path(method: str, path: str) -> bool:
67
+ if method != "GET":
68
+ return False
69
+ parts = [part for part in path.split("/") if part]
70
+ return len(parts) == 5 and parts[:3] == ["api", "v1", "contents"] and parts[3].isdigit() and parts[4] == "comments"
71
+
72
+
73
+ def _normalize_path(path: str) -> str:
74
+ value = "/" + path.strip().split("?", 1)[0].lstrip("/")
75
+ while "//" in value:
76
+ value = value.replace("//", "/")
77
+ return value
@@ -0,0 +1,14 @@
1
+ """Generic media adapter factory.
2
+
3
+ Local file media is currently routed through the existing ``generic_media``
4
+ platform. This module lets the registry discover a concrete local-audio
5
+ adapter without editing the shared registry.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from openmd.adapters.local_audio import LocalAudioAdapter
11
+
12
+
13
+ def create_adapter(platform_id: str, default_backend: str) -> LocalAudioAdapter:
14
+ return LocalAudioAdapter(platform_id, default_backend)
@@ -0,0 +1,77 @@
1
+ """Shared high-privacy export/cache status helpers.
2
+
3
+ High-privacy social/chat lanes must report operator-actionable states without
4
+ claiming ownership of platform sessions, browser profiles, databases, cookies,
5
+ or decrypted local stores.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from openmd.core.models import TranscriptResult
13
+ from openmd.core.secrets import sanitize_error_summary
14
+
15
+
16
+ HIGH_PRIVACY_STATUS_KEY = "high_privacy_status"
17
+
18
+
19
+ def high_privacy_status_metadata(
20
+ *,
21
+ platform: str,
22
+ target_kind: str,
23
+ state: str,
24
+ next_action: str,
25
+ permission_scope: str | None = None,
26
+ ) -> dict[str, Any]:
27
+ return {
28
+ "platform": platform,
29
+ "target_kind": target_kind,
30
+ "state": state,
31
+ "permission_scope": permission_scope or "unknown",
32
+ "next_action": sanitize_error_summary(next_action),
33
+ "core_owns_session": False,
34
+ "reads_private_database": False,
35
+ "decrypts_database": False,
36
+ "exports_cookies": False,
37
+ "persists_private_cache": False,
38
+ "requires_explicit_scope": True,
39
+ }
40
+
41
+
42
+ def high_privacy_blocker_result(
43
+ *,
44
+ platform: str,
45
+ backend: str,
46
+ target_kind: str,
47
+ state: str,
48
+ error_code: str,
49
+ error_summary: str,
50
+ next_action: str,
51
+ permission_scope: str | None = None,
52
+ title: str | None = None,
53
+ source_notes: list[str] | None = None,
54
+ ) -> TranscriptResult:
55
+ return TranscriptResult(
56
+ text="",
57
+ status="failed",
58
+ source_level="partial_extraction",
59
+ backend=backend,
60
+ title=title,
61
+ source_notes=[
62
+ *(source_notes or []),
63
+ "Openmd core did not read private databases, decrypt stores, export cookies, or own platform sessions.",
64
+ ],
65
+ error_code=error_code,
66
+ error_summary=sanitize_error_summary(error_summary),
67
+ metadata={
68
+ "failure_class": "user_resolvable",
69
+ HIGH_PRIVACY_STATUS_KEY: high_privacy_status_metadata(
70
+ platform=platform,
71
+ target_kind=target_kind,
72
+ state=state,
73
+ next_action=next_action,
74
+ permission_scope=permission_scope,
75
+ ),
76
+ },
77
+ )
@@ -0,0 +1,14 @@
1
+ """Local audio adapter lane.
2
+
3
+ The detector currently routes media-like inputs to ``generic_media``. The
4
+ ``generic_media`` adapter module reuses this local-audio adapter for local files
5
+ without requiring platform lanes to edit the shared registry.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .adapter import LocalAudioAdapter
11
+
12
+
13
+ def create_adapter(platform_id: str, default_backend: str) -> LocalAudioAdapter:
14
+ return LocalAudioAdapter(platform_id, default_backend)